// Copyright 2013 Daniel de Kok
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package eu.danieldk.dictomaton.collections;

import java.util.AbstractMap;
import java.util.AbstractSet;
import java.util.Collection;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;

import eu.danieldk.dictomaton.DictionaryBuilder;
import eu.danieldk.dictomaton.DictionaryBuilderException;
import eu.danieldk.dictomaton.PerfectHashDictionary;

/**
 * An immutable mapping from {@link String} to {@link String}, where both keys an values are compactly stored
 * using a finite state automaton.
 */
public class ImmutableStringStringMap extends AbstractMap<String, String>
{
	private final PerfectHashDictionary d_keys;
	private final PerfectHashDictionary d_values;
	private final int[] d_link;

	/**
	 * A builder for {@link ImmutableStringStringMap}. Mappings
	 * can be added to the builder using the {@link #put} and {@link #putAll} methods. The
	 * {@link ImmutableStringStringMap} can then be constructed using the {@link #build} method.
	 */
	public static class Builder
	{
		private final TreeMap<String, String> d_map;

		public Builder()
		{
			d_map = new TreeMap<>();
		}

		/**
		 * Put a key/value pair.
		 */
		public synchronized Builder put(String key, String value)
		{
			d_map.put(key, value);
			return this;
		}

		/**
		 * Put all key/value pairs from a {@link java.util.Map}.
		 */
		public synchronized Builder putAll(Map<String, String> map)
		{
			d_map.putAll(map);
			return this;
		}

		/**
		 * Construct a {@link ImmutableStringStringMap}.
		 */
		public synchronized ImmutableStringStringMap build() throws DictionaryBuilderException
		{
			PerfectHashDictionary keyDict = new DictionaryBuilder().addAll(d_map.keySet()).buildPerfectHash(false);
			PerfectHashDictionary valueDict = new DictionaryBuilder().addAll(new TreeSet<>(d_map.values()))
					.buildPerfectHash(false);

			int links[] = new int[keyDict.size()];

			for (Map.Entry<String, String> entry : d_map.entrySet())
			{
				int keyHashCode = keyDict.number(entry.getKey());
				int valueHashCode = valueDict.number(entry.getValue());

				links[keyHashCode - 1] = valueHashCode;
			}

			return new ImmutableStringStringMap(keyDict, valueDict, links);
		}

	}

	private class EntrySet extends AbstractSet<Entry<String, String>>
	{
		private class EntrySetIterator implements Iterator<Entry<String, String>>
		{
			private final Iterator<String> d_keyIter;

			public EntrySetIterator()
			{
				d_keyIter = d_keys.iterator();
			}

			@Override
			public boolean hasNext()
			{
				return d_keyIter.hasNext();
			}

			@Override
			public Entry<String, String> next()
			{
				String key = d_keyIter.next();
				int idx = d_keys.number(key) - 1;
				return new SimpleEntry<>(key, d_values.sequence(d_link[idx]));
			}

			@Override
			public void remove()
			{
				throw new UnsupportedOperationException();
			}
		}

		@Override
		public boolean contains(Object o)
		{
			if (o == null)
				return false;

			if (!(o instanceof Entry))
				return false;

			Entry e = (Entry) o;

			// Null keys and values are not supported.
			if (e.getKey() == null || e.getKey() == null)
				return false;

			if (!(e.getKey() instanceof String) || !(e.getValue() instanceof String))
				return false;

			String key = (String) e.getKey();
			String value = (String) e.getValue();

			int hash = d_keys.number(key);

			// Does not contain the key.
			if (hash == -1)
				return false;

			return d_values.sequence(d_link[hash - 1]).equals(value);
		}

		@Override
		public boolean isEmpty()
		{
			return d_keys.isEmpty();
		}

		@Override
		public Iterator<Entry<String, String>> iterator()
		{
			return new EntrySetIterator();
		}

		@Override
		public int size()
		{
			return d_keys.size();
		}
	}

	@Override
	public void clear()
	{
		throw new UnsupportedOperationException();
	}

	@Override
	public boolean containsKey(Object o)
	{
		return d_keys.contains(o);
	}

	@Override
	public Set<Entry<String, String>> entrySet()
	{
		return new EntrySet();
	}

	@Override
	public String get(Object o)
	{
		if (!(o instanceof String))
			return null;

		String key = (String) o;

		int hashcode = d_keys.number(key);
		if (hashcode == -1)
			return null;

		return d_values.sequence(d_link[hashcode - 1]);
	}

	@Override
	public boolean isEmpty()
	{
		return d_keys.isEmpty();
	}

	@Override
	public Set<String> keySet()
	{
		return d_keys;
	}

	@Override
	public String put(String k, String v)
	{
		throw new UnsupportedOperationException();
	}

	@Override
	public void putAll(Map<? extends String, ? extends String> m)
	{
		throw new UnsupportedOperationException();
	}

	@Override
	public String remove(Object key)
	{
		throw new UnsupportedOperationException();
	}

	@Override
	public int size()
	{
		return d_keys.size();
	}

	/**
	 * Get an iterator over the keys in the mapping.
	 */
	public Iterator<String> keyIterator()
	{
		return d_keys.iterator();
	}

	@Override
	public Collection<String> values()
	{
        // Note: check that this is in correspondence to the contract of this method. We might
        // have to return duplicate values, although they don't exist in our case.
		return d_values;
	}

	private ImmutableStringStringMap(PerfectHashDictionary keys, PerfectHashDictionary values, int[] link)
	{
		this.d_keys = keys;
		this.d_values = values;
		this.d_link = link;
	}
}
